跳到主要内容

SpringCloud OpenFeign 通信工具

参考资料 SpringCloud+OpenFeign 参考资料 官方文档地址

Feign 是什么?

Feign 就是实现各个服务之间通信的工具(即 RPC),它的核心就是动态代理模式

注意:原本的 Feign 已经不维护了,现在使用的是社区推出的 OpenFeign

OpenFeign 是声明式,模版化 HTTP 的客户端,在 Ribbon 中我们使用的 HTTP 请求工具 RestTemplate 需要自己构建请求路径。我们过多的关注请求的过程,OpenFeign使用在 Application-client 时,可以使我们像使用 Dubbo时直接通过接口方法远程调用Application-service,不用构建请求(就是不用像之前那样通过调用 RestTemplate 来访问服务了)。

举个例子(具体细节看下面):

这里创建一个接口,把需要调用的服务写上面

@FeignClient("search")
public interface SearchClient {

// 把对应服务需要用到的API写在这个接口上
@GetMapping("/search")
String search();
}

然后注入这个接口,就可以像调用本地方法那样调用远程服务了

@Autowired
private SearchClient searchClient;

@GetMapping("/customer")
public String customer() {
String response = searchClient.search();
return "this is customer model use" + response;
}

Feign 和 Ribbon 的区别

其实 Feign 底层使用的就是 Ribbon,只不过前者是在 Ribbon的基础上进行了一次改进,采用接口的方式,将需要调用的其他服务的方法定义成抽象方法即可,不需要自己构建 http 请求。不过要注意的是抽象方法的注解、方法签名要和提供服务的方法完全一致。

可以看到内部依赖的依旧是 Ribbon

Ribbon: 是一个基于 HTTP 和 TCP 客户端 的负载均衡的工具。它可以在客户端 配置 RibbonServerList(服务端列表),然后自己构建 http 请求,模拟 http 请求然后使用 RestTemplate 发送给其他服务,步骤相当繁琐。

Feign: 是在 Ribbon 的基础上进行了一次改进,是一个使用起来更加方便的 HTTP 客户端。采用接口的方式, 只需要创建一个接口,然后在上面添加注解即可 ,将需要调用的其他服务的方法定义成抽象方法即可, 不需要自己构建 http 请求。然后就像是调用自身工程的方法调用,而感觉不到是调用远程方法,使得编写客户端变得非常容易。

OpenFeign 的程序流程

1、Application-service 向 Eureka-server 注册服务 2、Application-client 在 Eureka-server 发现服务 3、client 通过 OpenFeign 发送 http 请求远程调用服务

OpenFeign 的工作原理

  • 首先,如果你对某个接口定义了 @FeignClient 注解,Feign 就会针对这个接口创建一个动态代理
  • 接着你要是调用那个接口,本质就是会调用 Feign 创建的动态代理,这是核心中的核心
  • Feign 的动态代理会根据你在接口上的 @RequestMapping 等注解,来动态构造出你要请求的服务的地址
  • 最后针对这个地址,发起请求、解析响应

因为 Feign 底层是使用了 Ribbon 作为负载均衡的客户端,而 Ribbon 的负载均衡也是依赖于 eureka 获得各个服务的地址,所以要引入 eureka-client

用于服务之间的调用,之前的各个服务之间调用需要写具体的 HTTP 地址,使用了 Feign 声明式调用就像调用本地方法一样调用远程方法,无感知远程 HTTP 请求。

原理就是创建一个接口,再使用这个 Feign 远程连接到这个服务后,实现这个接口

配置客户端环境

注意:之前那个 Feign 不维护了,官方推荐使用这个 OpenFeign

只有消费者需要添加这个配置,生产者无效配置

<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<!--eureka client,如果使用的是别的服务注册中心可以把这个换掉-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

在客户端启动类上加入 @EnableFeignClients 注解,表示启动这个 FeignClient 服务

@EnableFeignClients // 启用 Feign
@SpringCloudApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}

配置客户端

创建一个和需要远程调用的服务相同的接口(客户端) 注意:这里的 @FeignClient 需要写明要调用的服务叫什么

@Component
@FeignClient("search")
public interface SearchClient {

// 把对应服务的接口写在这个接口上
@GetMapping("/search")
String search();
}

然后 Feign 就会去找在 @FeignClient 写上的服务里的同名方法(要先在 Eureka 里注册的服务)

@Autowired
private SearchClient searchClient;

@GetMapping("/customer")
public String customer() {
String response = searchClient.search();
return "this is customer model use" + response;
}

配置生产者

被调用方是无需修改代码的,只要有同名方法就行了(被调用方)

// 被调用方无需在启动类里加上 @EnableFeignClients
@RestController
public class SearchController {
@GetMapping("/search")
public String search() {
return "this is search model";
}
}

传递参数

如果传递的参数比较复杂时,默认会采用 POST 的请求方式

传递单个参数时,优先使用 @PathVariable,如果传递单个参数比较多,也可以采用 @RequestParam(不能省略 value 属性) 传递对象信息时,统一采用 JSON 的方式,添加 @RequestBody

编写客户端

先创建一个接口

@FeignClient("search")
public interface SearchClient {

// 把对应服务的接口写在这个接口上
@GetMapping("/search")
String search();


// 这里的 @PathVariable 必须指定它的 value
@GetMapping("/search/{id}")
Customer findById(@PathVariable(value = "id") Integer id);

@GetMapping("/getCustomer")
Customer getCustomer(@RequestParam Integer id, @RequestParam String name);

// 复杂的对象请求需要使用 Post 来接收
@PostMapping("/save")
Customer save(@RequestBody Customer customer);
}

这里随便创建一个实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Customer {
private Integer id;
private String name;
private Integer age;
}

对于复杂的请求需要使用 Post 来接收

@RestController
public class CustomerController {
@Autowired
private SearchClient searchClient;

@GetMapping("/customer")
public String customer() {
String response = searchClient.search();
return "this is customer model use" + response;
}


@GetMapping("/search/{id}")
public Customer findById(@PathVariable Integer id) {
return searchClient.findById(id);
}

@GetMapping("/getCustomer")
public Customer getCustomer(@RequestParam Integer id, @RequestParam String name) {
return searchClient.getCustomer(id,name);
}

// 注意:这里是 Get 请求
// 访问:http://localhost:8080/save?id=1&name=yyy&age=19
@GetMapping("/save")
public Customer save(Customer customer) {
return searchClient.save(customer);
}
}

编写被调用的服务端

@RestController
public class SearchController {
@GetMapping("/search")
public String search() {
return "this is search model";
}


@GetMapping("/search/{id}")
public Customer findById(@PathVariable Integer id) {
return new Customer(id, "张三", 18);
}

@GetMapping("/getCustomer")
public Customer getCustomer(@RequestParam Integer id, @RequestParam String name) {
return new Customer(id, name, 18);
}

// 注意:接收对象这里一定是 Post 请求(Feign 会自动把对象封装到 Body 里面)
// 因为这个是另一个服务调用的,而不是用户直接调用的,所以是直接传递了一个对象过来
@PostMapping("/save")
public Customer save(@RequestBody Customer customer) {
return customer;
}
}

找不到服务的情况

首先检查一下 yml 文件,这里是否拉取了远程的服务信息

eureka:
client:
register-with-eureka: true
fetch-registry: true # 这个如果为 false 则是不拉取!!要将其改成 true
service-url:
defaultZone: http://localhost:7001/eureka #单机版
# defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka #集群版

如果实在没有办法可以使用 url 参数硬编码访问的地址

@Component
// @FeignClient(value = "PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class)
@FeignClient(name = "PROVIDER-HYSTRIX-PAYMENT", url = "http://localhost:8001/")
public interface PaymentHystrixService {

@GetMapping("/payment/hystrix/ok/{id}")
String paymentInfoOK(@PathVariable("id") Integer id);

@GetMapping("/payment/hystrix/timeout/{id}")
String paymentInfoTimeOut(@PathVariable("id") Integer id);
}

超时控制

在生产者处创建一个接口模拟超时的情况

@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout(){
//暂停几秒钟线程
try{
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e){
e.printStackTrace();
}
return serverPort;
}

在消费者处去调用这个接口(省略接口创建过程了)

@GetMapping(value = "/consumer/payment/feign/timeout")
public String paymentFeignTimeout(){
//openfeign 底层 ribbon,客户端默认等待1秒钟
return paymentFeignService.paymentFeignTimeout();
}

调用测试发现,发生了超时报错

前面说过这个 Feign 底层就是 Ribbon,所以直接配置 Ribbon 就行了

# 自定义的生产者服务名称
PAYMENT-SERVICE:
# 设置feign 客户端超时时间(openFeign默认支持ribbon)
ribbon:
# 指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间(超时时间)
ReadTimeout: 5000
# 指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000

具体配置方式参考 Spring cloud系列六 Ribbon的功能概述、主要组件和属性文件配置

添加日志打印

参考资料 Feign 配置日志的打印级别

打印的效果

细粒度 Feign 的日志级别

针对每个微服务配置

1、java代码方式

在Feign 接口注解上面配置 configuration

@FeignClient(value = "user-center", configuration = UserCenterFeignConfiguration.class)
public interface UserCenterFeignClient {

@GetMapping("/users/{id}")
UserDTO findById(@PathVariable("id") Integer id);

}

定义 configuration 内容,也就是 feign 的日志级别

Logger有四种类型:

  • NONE(默认):不显示任何日志
  • BASIC:仅记录请求方法、URL、响应状态码及执行时间
  • HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息
  • FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据

通过注册 Bean 来设置日志记录级别

注意:在此方法上不需要 @Configuration 注解,否则会被所有的 FeignClient 共享,如果添加了注解,则需要将此类放到启动时扫描不到的包(和配置 Ribbon 时的策略那样)

public class UserCenterFeignConfiguration {

@Bean
Logger.Level feignLoggerLevel(){
// 设置日志
return Logger.Level.FULL;
}
}

将 Feign 的全路径在 application.yml 中配置

logging:
level:
# com.maybesuch.contentcenter.feignclient.UserCenterFeignClient: debug
com.maybesuch: debug # feign 日志以什么级别监控哪个接口

2、配置文件的方式

只需在 application.yml 中添加配置:

logging:
level:
# com.maybesuch.contentcenter.feignclient.UserCenterFeignClient: debug
com.maybesuch: debug

feign:
client:
config:
# 要调用服务的名称
user-center:
loggerLevel: full

全局日志级别的配置

1、java代码的方式

在启动类 @EnableFeignClients 注解上配置 defaultConfiguration

@SpringBootApplication
@EnableFeignClients(defaultConfiguration = GlobalFeignConfiguration.class)
public class ContentCenterApplication {

public static void main(String[] args) {
SpringApplication.run(ContentCenterApplication.class, args);
}
}

定义 GlobalFeignConfiguration 类,此类也不需要 @Configuration 注解

public class GlobalFeignConfiguration {

@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}

配置文件 application.yml 中需配置日志级别方能打印出 Feign 调用的日志信息

logging:
level:
com.maybesuch: debug

2、配置文件方式

只需在 application.yml 中添加配置:

logging:
level:
com.maybecare: debug

feign:
client:
config:
# feign全局日志级别
default:
loggerLevel: full

Fallback 服务降级

这个 Fallback 是 Feign 和 Hystrix 结合的产物

Fallback 可以帮助我们在使用 Feign 去调用另一个服务时,如果出现了问题(例如报错,或者服务没有启动等),走服务降级,返回一个错误数据,避免功能因为一个服务出现问题导致全部失效

先创建一个用于服务降级的类,注意,这个类必须继承自下面的自定义的 SearchClient 接口

@Component
public class SearchClientFallBack implements SearchClient {

@Override
public String search() {
return "出现异常就会自动跳到这个类里来";
}

@Override
public Customer findById(Integer id) {
return null;
}

@Override
public Customer getCustomer(Integer id, String name) {
return null;
}

@Override
public Customer save(Customer customer) {
return null;
}
}

然后修改一下这个 SearchClient 接口,在 @FeignClient 注解的 fallback 参数加上要上面编写的类

@FeignClient(value = "search", fallback = SearchClientFallBack.class)
public interface SearchClient {
...

最后在配置文件上启动这个 FallBack 支持

# 让 FallBack 生效
feign:
hystrix:
enabled: true

FallBackFactory

单单使用上面的 FallBack ,调用方是无法知道具体的错误信息是什么的,如果还要返回错误信息给调用方,可以使用 FallBackFactory 的方式去实现该功能

创建一个工厂类去实现 FallbackFactory<Client> 接口

@Component
public class SearchClientFallBackFactory implements FallbackFactory<SearchClient> {
// 因为必须返回一个默认的 Fallback 出去,所以这里再自动注入一个进来
@Autowired
private SearchClientFallBack searchClientFallBack;

@Override
public SearchClient create(Throwable throwable) {
// 打印错误
throwable.printStackTrace();
return searchClientFallBack;
}
}

然后再次修改 SearchClient 接口

// 不再使用 fallback,而是使用 fallbackFactory
@FeignClient(value = "search", fallbackFactory = SearchClientFallBackFactory.class)
public interface SearchClient {
...

异常处理

用户请求时,服务调用流程。

平时编写代码时都是直接抛出一个异常,上层捕获处理,而到了 Feign 这种远距离调用如何处理异常呢?

微服务架构中,在正常的情况下,返回的数据结构是按照响应结构体返回的,但服务调用发生异常时,却返回不了code。 例子,在order-service调用product-service,由于库存不足,抛出异常,返回的结果如下:

{
"timestamp": "2021-08-19T03:43:53.452+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/login/register"
}

这时就需要自定义 ErrorDecoder 错误时返回统一的错误对象

import cn.hutool.json.JSONUtil;
import com.alsritter.common.api.ResultCode;
import com.alsritter.common.exception.BusinessException;
import feign.FeignException;
import feign.Response;
import feign.RetryableException;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

/**
* @author alsritter
* @version 1.0
**/
@Slf4j
@Configuration
public class FeignErrorDecoder extends ErrorDecoder.Default {

@Override
public Exception decode(String methodKey, Response response) {
Exception exception = super.decode(methodKey, response);

// 如果是 RetryableException,则返回继续重试
if (exception instanceof RetryableException) {
return exception;
}

try {
// 如果是 FeignException,则对其进行处理,并抛出 BusinessException
if (exception instanceof FeignException &&
((FeignException) exception).responseBody().isPresent()) {
ByteBuffer responseBody = ((FeignException) exception).responseBody().get();
String bodyText = StandardCharsets.UTF_8.newDecoder()
.decode(responseBody.asReadOnlyBuffer()).toString();
// 将异常信息,转换为 CommonResult 对象
CommonResult exceptionInfo = JSONUtil.toBean(bodyText, CommonResult.class);
// 如果 exception 中 code 不为空,则使用该 code,否则使用默认的错误code
Integer code = Optional
.ofNullable(exceptionInfo.getCode())
.orElse(ResultCode.FAILED.getCode());
// 如果 exception 中 message 不为空,则使用该 message,否则使用默认的错误message
String message = Optional
.ofNullable(exceptionInfo.getMessage())
.orElse(ResultCode.FAILED.getMessage());
return new BusinessException(code, message);
}
} catch (Exception ex) {
log.error(ex.getMessage(), ex);
}
return exception;
}
}

在 FeignClient 中使用 FeignErrorDecoder

@FeignClient(name = ServiceNameConstant.ORDER_SERVICE, configuration = {FeignErrorDecoder.class})

全局处理:在网关自定义 ErrorHandlerConfiguration

package tech.xproject.gateway.config;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;
import tech.xproject.gateway.handler.JsonExceptionHandler;

import java.util.Collections;
import java.util.List;

/**
* @author kent
*/
@Configuration
@EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
public class ErrorHandlerConfiguration {

private final ServerProperties serverProperties;
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;

public ErrorHandlerConfiguration(ServerProperties serverProperties, ResourceProperties resourceProperties,
ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer,
ApplicationContext applicationContext) {
this.serverProperties = serverProperties;
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(errorAttributes,
this.resourceProperties, this.serverProperties.getError(), this.applicationContext);
exceptionHandler.setViewResolvers(this.viewResolvers);
exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
return exceptionHandler;
}
}

Reference

Feign自定义ErrorDecoder错误时返回统一结构